<?php

	namespace org\tekuna\base;

	use \Exception;
	use \Logger;
	use \BadMethodCallException;

	use org\tekuna\base\classloader\ClassLoader;
	use org\tekuna\base\classloader\AbstractClassLoader;
	use org\tekuna\base\classloader\DirectoryClassLoader;
	use org\tekuna\base\classloader\PharClassLoader;


	/** A convenience constant for the base package that is just an alias for DIRECTORY_SEPARATOR. */
	const DS = DIRECTORY_SEPARATOR;


	/**
	 * This is the main Tekuna class that is responsible for:
	 *
	 *   - bootstrapping the framework
	 *   - class load triggering
	 *   - error handling
	 *   - exception handling
	 *
	 * The Tekuna class comes as a singleton which means that there is onle one
	 * instance which cannot be cloned. Additionally this class contains a
	 * convenience method that returns log4php loggers that are used by the framework.
	 */
	class Tekuna {

		/** This constant holds the current version of the Tekuna framework */
		const TEKUNA_VERSION = '0.3';


		private
			$arrClassLoaders = array(),
			$objLogger = NULL;


		/**
		 * This method gives you the singleton instance of Tekuna
		 *
		 * @return the Tekuna object
		 */
		public static function getInstance() {

			static $objInstance = NULL;

			if ($objInstance === NULL) {

				$objInstance = new Tekuna();
			}

			return $objInstance;
		}


		/**
		 * This is the main bootstrapping method. It does the following things:
		 *
		 *   - set maximum error reporting
		 *   - turns off HTML errors
		 *   - initializes the time zone to Europe/Berlin
		 *   - registers the exception handler
		 *   - registers the error handler
		 *   - registers the default directory class loader
		 *   - activates the classloading
		 *
		 * @param String $sApplicationBasedir the base directory of the Tekuna application.
		 */
		public static function bootstrap($sApplicationBasedir) {

			// set maximum error reporting
			error_reporting(E_ALL | E_STRICT);

			// turn off HTML in error messages
			ini_set('html_errors', false);

			// init the timezone settings
			date_default_timezone_set('Europe/Berlin');

			// instantiate Tekuna
			$objTekuna = Tekuna :: getInstance();

			// init logging log4php
			Logger :: configure($sApplicationBasedir . DS .'log4php.properties');
			$objTekuna -> objLogger = self :: getLogger(__CLASS__);
			$objTekuna -> objLogger -> info(str_repeat('#', 41 + strlen($sApplicationBasedir)));
			$objTekuna -> objLogger -> info("Initializing Tekuna in base directory '$sApplicationBasedir'.");
			$objTekuna -> objLogger -> info(str_repeat('#', 41 + strlen($sApplicationBasedir)));

			// register exception handler
			set_exception_handler(array('org\tekuna\base\Tekuna', 'handleException'));

			// register error handler
			set_error_handler(array('org\tekuna\base\Tekuna', 'handleError'));

			// register shutdown function to handle FATAL errors
			register_shutdown_function(array('org\tekuna\base\Tekuna', 'handleFatalError'));
			
			// register default class loader
			$objTekuna -> addClassLoader(new DirectoryClassLoader($sApplicationBasedir . DS .'src'));

			// init autoloading
			spl_autoload_register(array('org\tekuna\base\Tekuna', 'loadClass'));
		}


		/**
		 * Convenience method that returns a log4php Logger object. The initialization is performed
		 * by using a class name (as the context-dependent constant __CLASS__ holds it). The Logger
		 * then uses dots in the package path.
		 *
		 * @param String $sFullQualifiedClassName the name of the class including the package path in php namespace
		 */
		public static function getLogger($sFullQualifiedClassName) {

			return Logger :: getLogger(str_replace('\\', '.', $sFullQualifiedClassName));
		}


		/**
		 * Method to load a specified class with the currently registered ClassLoaders. This method
		 * is triggered automatically by the __autoload() functionality.
		 *
		 * @param String $sFullQualifiedClassName the class with package path to load
		 * @return boolean success
		 */
		public static function loadClass($sFullQualifiedClassName) {

			$sNamespace = AbstractClassLoader :: extractNamespace($sFullQualifiedClassName);
			$sClassName = AbstractClassLoader :: extractClassName($sFullQualifiedClassName);

			foreach (Tekuna :: getInstance() -> arrClassLoaders as $objClassLoader) {

				// invoke the class loader
				$objClassLoader -> loadClass($sNamespace, $sClassName);

				if (class_exists($sFullQualifiedClassName, false)) {

					// log the successful class load on debug
					Tekuna :: getInstance() -> objLogger -> debug("Successfully loaded class '$sFullQualifiedClassName' with class loader $objClassLoader.");

					// class successfully loaded
					return true;
				}

				if (interface_exists($sFullQualifiedClassName, false)) {

					// log the successful class load on debug
					Tekuna :: getInstance() -> objLogger -> debug("Successfully loaded interface '$sFullQualifiedClassName' with class loader $objClassLoader.");

					// class successfully loaded
					return true;
				}
			}

			// class could not be loaded
			// do not throw an exception
			// the spl autoload stack may contain further autoloaders...
			Tekuna :: getInstance() -> objLogger -> warn("Could not load class '$sFullQualifiedClassName' with registered class loaders.");
			return false;
		}


		/**
		 * Error handler that converts incoming PHP errors into exceptions that are thrown and that may be catched
		 * afterwards. For each of the following errors a separately named Exception is thrown:
		 *
		 *   - NOTICE
		 *   - WARNING
		 *   - USER_ERROR
		 *   - USER_WARNING
		 *   - USER_NOTICE
		 *   - STRICT
		 *   - RECOVERABLE_ERROR
		 *   - DEPRECATED
		 *   - USER_DEPRECATED
		 *
		 * This method is used as the error handler
		 *
		 * @param int $iSeverity the error level
		 * @param String $sMessage the error message
		 * @param String $sFile the file that contains the error
		 * @param int $iLine the line on which the error occured
		 */
		public static function handleError($iSeverity, $sMessage, $sFile, $iLine) {

			// check the current reporting bitmap
			$bHandleError = error_reporting() & $iSeverity;
			if (! $bHandleError) {

				return;
			}

			// get error code name
			$sExceptionClass = '\Exception';
			$sExceptionNamespace = 'org\tekuna\base\exception\\';
			switch ($iSeverity) {

				case E_NOTICE: $sExceptionClass = $sExceptionNamespace .'PHPNoticeException'; break;
				case E_WARNING: $sExceptionClass = $sExceptionNamespace .'PHPWarningException'; break;
				case E_USER_ERROR: $sExceptionClass = $sExceptionNamespace .'PHPUserErrorException'; break;
				case E_USER_WARNING: $sExceptionClass = $sExceptionNamespace .'PHPUserWarningException'; break;
				case E_USER_NOTICE: $sExceptionClass = $sExceptionNamespace .'PHPUserNoticeException'; break;
				case E_STRICT: $sExceptionClass = $sExceptionNamespace .'PHPStrictException'; break;
				case E_RECOVERABLE_ERROR: $sExceptionClass = $sExceptionNamespace .'PHPRecoverableErrorException'; break;
				case E_DEPRECATED: $sExceptionClass = $sExceptionNamespace .'PHPDeprecatedException'; break;
				case E_USER_DEPRECATED: $sExceptionClass = $sExceptionNamespace .'PHPUserDeprecatedException'; break;
			}

			// log this conversion to debug
			Tekuna :: getInstance() -> objLogger -> debug("Converted PHP Error #$iSeverity to Exception of type '$sExceptionClass'.");

			// throw special exception with the severity as error code
			throw new $sExceptionClass($sMessage, $iSeverity);
		}

		
		/**
		 * Logs the given exception on fatal level
		 * 
		 * @param Exception $objException
		 */
		public static function logException(Exception $objException) {
			
			// log the exception as fatal error
			Tekuna :: getInstance() -> objLogger -> fatal("Uncaught Exception: ". $objException);
		}
		
		
		/**
		 * This method is registered as shutdown function. Its only purpose is
		 * to check the last occured error and if that error was FATAL, write
		 * the information to the log.
		 */
		public static function handleFatalError() {
			
			// get last error
			$arrLastError = error_get_last();
			
			// check that this is a FATAL error
			if ($arrLastError != null && isset($arrLastError['type']) && $arrLastError['type'] == E_ERROR) {

				// write message to log
				Tekuna :: getInstance() -> objLogger -> fatal("FATAL ERROR: $arrLastError[message] in $arrLastError[file] on line $arrLastError[line].");
			}
		}
		

		/**
		 * Exception handler that just loggs the incoming exception with FATAL log level.
		 *
		 * @param Exception $objException the exception to handle
		 */
		public static function handleException(Exception $objException) {

			// log the exception
			self :: logException($objException);

			// handle CLI version
			if (defined('STDIN')) {

				// echo exception without stack trace
				echo "\n\n";
				echo "========================================================\n";
				echo "=== Application aborted due to an uncaught Exception ===\n";
				echo "========================================================\n";

				// print all causes
				$objCurrentException = $objException;

				do {

					if ($objCurrentException != $objException) {

						echo "\ncaused by:\n";
					}

					echo "\n== ". get_class($objCurrentException) ." ==\n";
					echo $objCurrentException -> getMessage() ."\n";
					$objCurrentException = $objCurrentException -> getPrevious();

				} while ($objCurrentException != NULL);

				exit(1);
			}
			else {

				// handle HTTP version

				// send error status header; prevent another error when headers are already sent
				@header('HTTP/1.1 500 Internal Server Error');

				// echo exception without stack trace
				echo '<h1>Application aborted due to an uncaught Exception</h1>';

				// print all causes
				$objCurrentException = $objException;

				do {

					if ($objCurrentException != $objException) {

						echo '<p>caused by:</p>';
					}

					echo '<h2>'. get_class($objCurrentException) .'</h2>';
					echo '<p><pre>'. $objCurrentException -> getMessage() .'</pre></p>';
					$objCurrentException = $objCurrentException -> getPrevious();

				} while ($objCurrentException != NULL);
			}
		}


		/**
		 * This method adds a new class loader to the class loader chain that is traversed
		 * when the loadClass() method is triggered. Class loaders are asked to load a specified
		 * class in the order they were registered.
		 *
		 * @param ClassLoader $objNewClassLoader the new ClassLoader
		 */
		public function addClassLoader(ClassLoader $objNewClassLoader) {

			$this -> objLogger -> info("Adding ClassLoader $objNewClassLoader.");
			$this -> arrClassLoaders[] = $objNewClassLoader;
		}
		
		
		/**
		 * @return Returns List of registered Class Loaders
		 */
		public function getRegisteredClassLoaders() {
			
			return $this -> arrClassLoaders;
		}


		/**
		 * Because of the singleton nature of this class, it cannot be cloned.
		 * @throws BadMethodCallException without condition
		 */
		final public function __clone() {

			throw new BadMethodCallException("Class Tekuna is a singleton. Cloning this object is not possible.");
		}
	}
